﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using PostSharp.Laos;
using B4ALL.Cache.Singleton.Contract;
using Spring.Context;
using Spring.Context.Support;
using B4ALL.JITL;
using B4ALL.Common;

namespace B4ALL.Cache.Singleton
{
    public class CacheSingletonService : JITLService, Contract.ICacheSingletonService, ICacheHandler
    {

        #region props

        private IDataCacheManager cacheManager;

        public IDataCacheManager CacheManager
        {
            get { return cacheManager; }
            set { cacheManager = value; }
        }

        #endregion props

        #region Contract.ICacheSingletonService

        /// <summary>
        /// Is a class/instance uniquely identifiable in the cache system
        /// </summary>
        /// <param name="t">Type of the class/instance</param>
        /// <returns></returns>
        public Boolean IsSingleton(Type t)
        {
            if (t.GetInterface(typeof(ICacheSingleton).Name) != null)
                return true;

            return false;
        }

        /// <summary>
        /// Based on a method specs, can it determine (its cache key) and returns a singleton element
        /// True if return type implements ICacheSingleton, and method has CacheSingletonIdentifier or parameters are only value types
        /// </summary>
        /// <param name="eventArgs"></param>
        /// <returns></returns>
        public Boolean IsSingleton(MethodExecutionEventArgs eventArgs)
        {
            if (IsSingleton(((MethodInfo)eventArgs.Method).ReturnType))
                return true;

            //if (eventArgs.Method.IsDefined(typeof(CacheSingletonIdentifier), false))
            //    return true;

            //ParameterInfo[] parameters = eventArgs.Method.GetParameters();

            //foreach (ParameterInfo param in parameters)
            //{
            //    if (!param.ParameterType.IsValueType && param.ParameterType.GetInterface(typeof(SubSonic.IAbstractRecord).Name) == null)
            //        return false;
            //}

            //return true;

            return false;
        }

        /// <summary>
        /// Returns unique cache key for a UniqueCacheRessource
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public CacheSingletonKey GetKey(ICacheSingleton obj)
        {

            Type objType = obj.GetType();

            CacheSingletonKey key = new CacheSingletonKey(objType.Name);

            List<PropertyInfo> properties = GetIdentificationProperties(objType);

            foreach (PropertyInfo propInfo in properties)
            {
                CacheSingletonIdentifier cacheIdentifier = (CacheSingletonIdentifier)propInfo.GetCustomAttributes(typeof(CacheSingletonIdentifier), false)[0];
                Object invokeResult = objType.InvokeMember(propInfo.Name, BindingFlags.GetProperty, null, obj, null);
                key.Parameters.Add(cacheIdentifier.AbstractName, invokeResult);
            }

            return key;
        }

        /// <summary>
        /// Build unique key from method parameters, using uniquecacheparameter attributes
        /// </summary>
        /// <param name="eventArgs">Method reflected info</param>
        /// <returns></returns>
        public CacheSingletonKey GetKey(MethodExecutionEventArgs eventArgs)
        {
            CacheSingletonKey key = new CacheSingletonKey(((MethodInfo)eventArgs.Method).ReturnType.Name);

            Dictionary<CacheSingletonIdentifier, Object> properties = GetIdentificationParameters(eventArgs);

            foreach (KeyValuePair<CacheSingletonIdentifier, Object> keyPair in properties)
            {
                //if (keyPair.Value != null && keyPair.Value.GetType().GetInterface(typeof(SubSonic.IAbstractRecord).Name) != null)
                //    key.Parameters.Add(keyPair.Key.AbstractName, ((SubSonic.IAbstractRecord)keyPair.Value).GetPrimaryKeyValue());
                //else
                key.Parameters.Add(keyPair.Key.AbstractName, keyPair.Value);
            }

            return key;
        }

        public ICacheSingleton NewDummyObject(MethodExecutionEventArgs eventArgs)
        {

            ICacheSingleton dummyObject = null;

            if (eventArgs.ReturnValue != null && IsSingleton(eventArgs.ReturnValue.GetType()))
                dummyObject = (ICacheSingleton)System.Activator.CreateInstance(eventArgs.ReturnValue.GetType());
            else if (IsSingleton(eventArgs))
                dummyObject = (ICacheSingleton)System.Activator.CreateInstance(((MethodInfo)eventArgs.Method).ReturnType);

            if (dummyObject != null)
            {
                dummyObject.Reference = new CacheSingletonReference((MethodInfo)eventArgs.Method, eventArgs.GetArguments(), GetKey(eventArgs));
            }

            return dummyObject;

        }

        /// <summary>
        /// Retrieves object from its ref
        /// Determines whether the object is already in cache or if the method has to be executed
        /// </summary>
        /// <param name="dummyTargetSrc"></param>
        public override void LoadByRef(Object dummyTargetSrc)
        {

            if (dummyTargetSrc.GetType().GetInterface(typeof(ICacheSingleton).Name) == null)
                throw new CacheSingletonException();

            JITLContext ctx = Context.GetContext<JITLContext>();
            if (ctx == null)
                Context.SetContext(new JITLContext());

            Context.GetContext<JITLContext>().DelayLoad = false;

            ICacheSingleton dummyTarget = (ICacheSingleton)dummyTargetSrc;

            Object realObject = null;

            realObject = cacheManager.GetRegex(dummyTarget.Reference.Key.ToRegex());

            if (realObject == null)
            {
                base.LoadByRef(dummyTargetSrc);
            }
            else
            {
                dummyTarget.Copy(ref realObject, ref dummyTargetSrc);
            }


            Context.GetContext<JITLContext>().DelayLoad = true;

        }

        #endregion Contract.ICacheSingletonService

        #region Helpers

        /// <summary>
        /// Returns property infos for the identifying properties
        /// </summary>
        /// <param name="t">Type of the class/instance</param>
        /// <returns></returns>
        internal List<PropertyInfo> GetIdentificationProperties(Type t)
        {
            PropertyInfo[] properties = t.GetProperties();
            List<PropertyInfo> idProperties = new List<PropertyInfo>();

            foreach (PropertyInfo propInfo in properties)
            {
                if (propInfo.IsDefined(typeof(CacheSingletonIdentifier), true))
                {
                    idProperties.Add(propInfo);
                    continue;
                }
            }

            return idProperties;

        }

        /// <summary>
        /// Returns ordered id properties for a specific method
        /// ID properties get ordered according to ID properties specified in returned class (i.e. MerchantProductDto)
        /// </summary>
        /// <param name="eventArgs"></param>
        /// <returns></returns>
        private Dictionary<CacheSingletonIdentifier, Object> GetIdentificationParameters(MethodExecutionEventArgs eventArgs)
        {
            Object[] idAttributesInMethod = eventArgs.Method.GetCustomAttributes(typeof(CacheSingletonIdentifier), false);
            List<PropertyInfo> identificationPropertiesInObject = GetIdentificationProperties(((MethodInfo)eventArgs.Method).ReturnType);
            ParameterInfo[] methodParameters = eventArgs.Method.GetParameters();
            Object[] methodParameterValues = eventArgs.GetArguments();
            Dictionary<CacheSingletonIdentifier, Object> properties = new Dictionary<CacheSingletonIdentifier, Object>();

            //1st case: identification attributes are explicitely defined in method header
            if (idAttributesInMethod.Length > 0)
            {
                foreach (PropertyInfo propertyInObject in identificationPropertiesInObject)
                {
                    CacheSingletonIdentifier identifierInObjectProperty = (CacheSingletonIdentifier)(propertyInObject.GetCustomAttributes(typeof(CacheSingletonIdentifier), true)[0]);

                    Boolean foundAttributeInMethod = false;

                    foreach (Object idAttributeInMethod in idAttributesInMethod)
                    {
                        if (identifierInObjectProperty.AbstractName == ((CacheSingletonIdentifier)idAttributeInMethod).AbstractName)
                        {
                            foundAttributeInMethod = true;

                            for (Int32 i = 0; i < methodParameters.Length; i++)
                            {
                                if (methodParameters[i].Name == ((CacheSingletonIdentifier)idAttributeInMethod).NameInMethod && String.IsNullOrEmpty(((CacheSingletonIdentifier)idAttributeInMethod).PropertyNameInMethod))
                                {
                                    properties.Add(identifierInObjectProperty, methodParameterValues[i]);
                                    break;
                                }
                                else if (methodParameters[i].Name == ((CacheSingletonIdentifier)idAttributeInMethod).NameInMethod && !String.IsNullOrEmpty(((CacheSingletonIdentifier)idAttributeInMethod).PropertyNameInMethod) && !methodParameters[i].GetType().IsValueType)
                                {
                                    properties.Add(identifierInObjectProperty, methodParameterValues[i].GetType().InvokeMember(((CacheSingletonIdentifier)idAttributeInMethod).PropertyNameInMethod, BindingFlags.GetProperty, null, methodParameterValues[i], null));
                                    break;
                                }
                            }
                            break;
                        }
                    }

                    if (!foundAttributeInMethod)
                        properties.Add(identifierInObjectProperty, null);

                }
            }
            //2nd case: identification attributes are not defined. Parameters are automatically matched to cachesingleton parameters.
            else
            {
                List<PropertyInfo> primaryKeys = new List<PropertyInfo>();
                Int32 i = 0;
                
                for (i = 0 ; i < identificationPropertiesInObject.Count ; i ++)
                {
                    if (((CacheSingletonIdentifier)identificationPropertiesInObject[i].GetCustomAttributes(typeof(CacheSingletonIdentifier), false)[0]).Type == CacheSingletonIdentifierType.PrimaryKey)
                        primaryKeys.Add(identificationPropertiesInObject[i]);
                }

                i = 0;
                Int32 limit = methodParameterValues.Length < primaryKeys.Count ? methodParameterValues.Length : primaryKeys.Count;

                for (; i < limit; i++)
                {
                    if (!primaryKeys[i].PropertyType.Equals(methodParameterValues[0].GetType()) && methodParameterValues[0].GetType().IsValueType)
                        throw new CacheSingletonException();
                    //else if (methodParameterValues[0].GetType().GetInterface(typeof(SubSonic.IAbstractRecord).Name) != null && !primaryKeys[i].PropertyType.Equals(((SubSonic.IAbstractRecord)methodParameterValues[0]).GetPrimaryKeyValue().GetType()))
                    //    throw new CacheSingletonException();
                    //else if (!methodParameterValues[0].GetType().IsValueType && methodParameterValues[0].GetType().GetInterface(typeof(SubSonic.IAbstractRecord).Name) == null)
                    //    throw new CacheSingletonException();
                    else if (!methodParameterValues[0].GetType().IsValueType)
                        throw new CacheSingletonException();

                    properties.Add((CacheSingletonIdentifier)primaryKeys[i].GetCustomAttributes(typeof(CacheSingletonIdentifier), false)[0], methodParameterValues[i]);
                }

                for (; i < primaryKeys.Count; i++)
                {
                    properties.Add((CacheSingletonIdentifier)primaryKeys[i].GetCustomAttributes(typeof(CacheSingletonIdentifier), false)[0], null);
                }
            }

            return properties;

        }

        #endregion Helpers

        #region ICacheHandler Members

        public HandlerBehavior OnEntry(MethodExecutionEventArgs eventArgs, OnMethodBoundaryAspect aspect)
        {

            JITLContext jitlContext = Context.GetContext<JITLContext>();

            Object result = null;

            if (IsSingleton(eventArgs))
            {
                if (jitlContext == null || (jitlContext != null && jitlContext.DelayLoad))
                {
                    result = NewDummyObject(eventArgs);
                }
                else if (jitlContext != null && !jitlContext.DelayLoad)
                {
                    CacheSingletonKey key = GetKey(eventArgs);
                    result = cacheManager.GetRegex(key.ToRegex());
                }

                if (result != null && result.GetType() == ((MethodInfo)eventArgs.Method).ReturnType)
                {
                    eventArgs.ReturnValue = result;
                    eventArgs.FlowBehavior = FlowBehavior.Return;
                }

                return HandlerBehavior.Break;
            }

            return HandlerBehavior.Continue;

        }

        public HandlerBehavior OnSuccess(MethodExecutionEventArgs eventArgs, OnMethodBoundaryAspect aspect)
        {
            if (eventArgs.ReturnValue != null && IsSingleton(eventArgs.ReturnValue.GetType()))
            {
                CacheContext cacheContext = Context.GetContext<CacheContext>();

                lock (cacheManager)
                {
                    CacheSingletonKey key = GetKey((ICacheSingleton)eventArgs.ReturnValue);

                    if (cacheContext != null && cacheContext.Duration.HasValue)
                        cacheManager.Add(key.ToString(), eventArgs.ReturnValue, cacheContext.Duration.Value);
                    else
                        cacheManager.Refresh(key.ToString(), eventArgs.ReturnValue);
                }

                return HandlerBehavior.Break;
            }

            return HandlerBehavior.Continue;
        }

        #endregion
    }
}